跳到主要内容

DDD 分层架构

1. DDD 分层架构

问题:请描述 DDD 的四层架构

参考答案:

DDD 分层架构包含四个层次:用户接口层、应用层、领域层和基础设施层。

各层职责:

  • 用户接口层:处理HTTP请求、参数验证、数据序列化
  • 应用层:协调业务流程、事务管理、权限控制,不包含业务逻辑
  • 领域层:核心业务逻辑,包含实体、值对象、聚合、领域服务
  • 基础设施层:技术实现,如数据库访问、外部服务调用

问题:在 Go 项目中如何组织 DDD 的目录结构?

参考答案:

project/
├── interfaces/ # 用户接口层
│ ├── http/ # HTTP 接口
│ ├── grpc/ # gRPC 接口
│ └── message/ # 消息队列
├── application/ # 应用层
│ ├── service/ # 应用服务
│ └── dto/ # 数据传输对象
├── domain/ # 领域层
│ ├── user/ # 用户聚合
│ │ ├── entity/ # 实体
│ │ ├── service/ # 领域服务
│ │ ├── repository/ # 仓储接口
│ │ └── event/ # 领域事件
│ └── order/ # 订单聚合
└── infrastructure/ # 基础设施层
├── repository/ # 仓储实现
├── config/ # 配置
└── client/ # 外部客户端

2. 聚合和实体

问题:什么是聚合?聚合根的作用是什么?请用 Go 代码示例说明

参考答案:

聚合是一组相关对象的集合,用于维护数据一致性和业务规则。聚合根是聚合的入口点,负责管理聚合内对象的一致性。

// 订单聚合根
type Order struct {
id OrderID
customerID CustomerID
items []*OrderItem
status OrderStatus
totalPrice Money
createdAt time.Time
}

// 订单项实体
type OrderItem struct {
id OrderItemID
productID ProductID
quantity int
price Money
}

// 聚合根方法 - 添加订单项
func (o *Order) AddItem(productID ProductID, quantity int, price Money) error {
if o.status != OrderStatusDraft {
return errors.New("只能向草稿状态的订单添加商品")
}

item := &OrderItem{
id: NewOrderItemID(),
productID: productID,
quantity: quantity,
price: price,
}

o.items = append(o.items, item)
o.calculateTotalPrice()
return nil
}

// 内部方法维护一致性
func (o *Order) calculateTotalPrice() {
total := Money(0)
for _, item := range o.items {
total += Money(item.quantity) * item.price
}
o.totalPrice = total
}

问题:聚合设计的边界原则是什么?

参考答案:

聚合边界设计原则:

  1. 事务边界:一个聚合内的修改在同一个事务中完成
  2. 不变性约束:聚合内的业务规则必须始终保持一致
  3. 小聚合原则:聚合应该尽可能小,避免性能问题
  4. 通过ID引用:聚合间通过ID引用,不直接持有对象引用

3. 值对象和实体

问题:值对象和实体的区别是什么?请用 Go 代码举例说明

参考答案:

实体:有唯一标识,可变,有生命周期 值对象:无标识,不可变,通过属性值判断相等性

// 实体 - 用户
type User struct {
id UserID // 唯一标识
username string
email Email // 值对象
address Address // 值对象
version int // 乐观锁版本号
}

func (u *User) ID() UserID {
return u.id
}

func (u *User) ChangeEmail(newEmail Email) error {
if !newEmail.IsValid() {
return errors.New("无效的邮箱地址")
}
u.email = newEmail
return nil
}

// 值对象 - 邮箱
type Email struct {
value string
}

func NewEmail(email string) (Email, error) {
if !isValidEmail(email) {
return Email{}, errors.New("无效的邮箱格式")
}
return Email{value: email}, nil
}

func (e Email) String() string {
return e.value
}

func (e Email) Equals(other Email) bool {
return e.value == other.value
}

func (e Email) IsValid() bool {
return isValidEmail(e.value)
}

// 值对象 - 地址
type Address struct {
province string
city string
district string
detail string
}

func (a Address) Equals(other Address) bool {
return a.province == other.province &&
a.city == other.city &&
a.district == other.district &&
a.detail == other.detail
}

4. 领域服务

问题:什么时候需要使用领域服务?请举例说明

参考答案:

当业务逻辑不适合放在单个实体或值对象中时,需要使用领域服务:

  1. 跨多个聚合的业务逻辑
  2. 复杂的业务规则
  3. 无状态的领域操作
// 领域服务 - 转账服务
type TransferService struct {
accountRepo AccountRepository
}

func (s *TransferService) Transfer(
fromAccountID, toAccountID AccountID,
amount Money,
) error {
// 跨聚合的业务逻辑
fromAccount, err := s.accountRepo.FindByID(fromAccountID)
if err != nil {
return err
}

toAccount, err := s.accountRepo.FindByID(toAccountID)
if err != nil {
return err
}

// 业务规则检查
if fromAccount.Balance().LessThan(amount) {
return errors.New("余额不足")
}

if fromAccount.IsBlocked() || toAccount.IsBlocked() {
return errors.New("账户被冻结")
}

// 执行转账
if err := fromAccount.Withdraw(amount); err != nil {
return err
}

if err := toAccount.Deposit(amount); err != nil {
// 回滚
fromAccount.Deposit(amount)
return err
}

return nil
}

5. 领域事件

问题:领域事件的作用是什么?如何在 Go 中实现领域事件?

参考答案:

领域事件用于:

  • 解耦聚合间的通信
  • 实现最终一致性
  • 支持事件溯源
  • 触发副作用操作
// 领域事件接口
type DomainEvent interface {
EventType() string
AggregateID() string
OccurredOn() time.Time
}

// 用户注册事件
type UserRegisteredEvent struct {
userID UserID
email Email
occurredOn time.Time
}

func (e UserRegisteredEvent) EventType() string {
return "UserRegistered"
}

func (e UserRegisteredEvent) AggregateID() string {
return e.userID.String()
}

func (e UserRegisteredEvent) OccurredOn() time.Time {
return e.occurredOn
}

// 聚合根基类
type AggregateRoot struct {
events []DomainEvent
}

func (ar *AggregateRoot) RaiseEvent(event DomainEvent) {
ar.events = append(ar.events, event)
}

func (ar *AggregateRoot) GetEvents() []DomainEvent {
return ar.events
}

func (ar *AggregateRoot) ClearEvents() {
ar.events = nil
}

// 用户聚合
type User struct {
AggregateRoot
id UserID
email Email
}

func (u *User) Register(email Email) {
u.email = email
// 发布事件
u.RaiseEvent(UserRegisteredEvent{
userID: u.id,
email: email,
occurredOn: time.Now(),
})
}

事件发布流程:

6. 限界上下文

问题:什么是限界上下文?如何在微服务架构中应用?

参考答案:

限界上下文定义了统一语言的边界,每个上下文内有自己的领域模型:

在 Go 中的实现:

// 订单上下文中的客户概念
package order

type Customer struct {
ID CustomerID
Name string
}

// 支付上下文中的客户概念
package payment

type Customer struct {
ID CustomerID
PaymentMethod string
CreditLimit Money
}

// 上下文间通过事件通信
type OrderCreatedEvent struct {
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
Amount int64 `json:"amount"`
}

7. 贫血模型 vs 充血模型

问题:贫血模型和充血模型的区别?在 Go 项目中如何选择?

参考答案:

贫血模型:数据和行为分离

// 贫血模型 - 只有数据
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Status int `json:"status"`
}

// 行为在服务层
type UserService struct {
userRepo UserRepository
}

func (s *UserService) ActiveUser(userID int64) error {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return err
}

user.Status = 1 // 激活状态
return s.userRepo.Save(user)
}

充血模型:数据和行为封装在一起

// 充血模型 - 包含业务逻辑
type User struct {
id UserID
username string
email Email
status UserStatus
}

func (u *User) Activate() error {
if u.status == UserStatusBlocked {
return errors.New("被封禁的用户无法激活")
}

u.status = UserStatusActive
return nil
}

func (u *User) Block(reason string) error {
if u.status == UserStatusBlocked {
return errors.New("用户已被封禁")
}

u.status = UserStatusBlocked
// 可以发布领域事件
return nil
}

选择建议

  • 简单CRUD项目:贫血模型,开发快速
  • 复杂业务逻辑:充血模型,更好的封装性
  • Go项目特点:Go的组合特性适合充血模型

8. 仓储模式

问题:Repository 模式的作用是什么?如何在 Go 中实现?

参考答案:

Repository 封装了数据访问逻辑,提供类似集合的接口:

// 领域层定义接口
type UserRepository interface {
Save(user *User) error
FindByID(id UserID) (*User, error)
FindByEmail(email Email) (*User, error)
Delete(id UserID) error
}

// 基础设施层实现
type MySQLUserRepository struct {
db *sql.DB
}

func (r *MySQLUserRepository) Save(user *User) error {
query := `
INSERT INTO users (id, username, email, status)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
username=VALUES(username),
email=VALUES(email),
status=VALUES(status)
`

_, err := r.db.Exec(query,
user.ID().String(),
user.Username(),
user.Email().String(),
user.Status(),
)
return err
}

func (r *MySQLUserRepository) FindByID(id UserID) (*User, error) {
query := "SELECT id, username, email, status FROM users WHERE id = ?"

var userID, username, email string
var status int

err := r.db.QueryRow(query, id.String()).Scan(
&userID, &username, &email, &status,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, ErrUserNotFound
}
return nil, err
}

emailVO, _ := NewEmail(email)
return ReconstructUser(
NewUserID(userID),
username,
emailVO,
UserStatus(status),
), nil
}

Reference